package com.ibm.cps.sql;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.ibm.cps.message.MessageFields;
import com.ibm.cps.newmessage.BasicDataSourceMetadata;
import com.ibm.cps.sql.ProjectionParser.DatasourceField;
import com.ibm.cps.sql.expression.BooleanExpression;
import com.ibm.cps.sql.expression.ExpressionNode;
import com.ibm.cps.sql.expression.IdentifierNode;
import com.ibm.cps.sql.expression.ValueExpression;
import com.ibm.factories.DataSourceFactory;
import com.ibm.interfaces.ITimeSeriesSchema;
import com.ibm.util.ErrorCode;
import com.ibm.util.JSONUtility;
import com.ibm.util.SchemaCache;
import com.ibm.util.exception.CPSException;

/**
 * Created by telekinesis on 6/5/15.
 */
public class NonWindowingRequestParser {
	private final DataSourceFactory datasourceFactory;
	private final SchemaCache schemaCache;
	private final String ruleName;
	private final String groupId;
	private final String tenantId;
	private final ProcessorMetadataCreator metaCreator;

	public NonWindowingRequestParser(DataSourceFactory datasourceFactory,
			SchemaCache schemaCache, String ruleName, String groupId,
			String tenantId) {
		this.datasourceFactory = datasourceFactory;
		this.schemaCache = schemaCache;
		this.ruleName = ruleName;
		this.groupId = groupId;
		this.tenantId = tenantId;
		this.metaCreator = new ProcessorMetadataCreator(tenantId, groupId, "sql");
	}

	public ArrayNode parseNonWindowingRequest(
			List<String> dataSources, SelectItemDescriptor selectItems, AggregationDescriptor aggregation, BooleanExpression filteringCondition,
			List<String> publishers)
					throws CPSException {
		ArrayNode result = JSONUtility.newArrayNode();
		
		ObjectNode inputProcessor = createInputProcessors(dataSources, groupId, tenantId);
		String inputProcessorId = inputProcessor.get(MessageFields.PROCESSORID).asText();
		result.add(inputProcessor);
		ObjectNode calculationNode = createCalculationNode(dataSources, selectItems, aggregation, filteringCondition, inputProcessorId);
		String lastProcessorId = calculationNode.get(MessageFields.PROCESSORID).asText();
		result.add(calculationNode);
		if (filteringCondition != null) {
			ObjectNode ruleNode = metaCreator.createNaiveRuleNode(lastProcessorId,
					false, filteringCondition, ruleName);
			lastProcessorId = ruleNode.get(MessageFields.PROCESSORID).asText();
			result.add(ruleNode);
		}
		
		ObjectNode finalProjection = createFinalProjection(dataSources, selectItems, lastProcessorId);
		lastProcessorId = finalProjection.get(MessageFields.PROCESSORID).asText();
		result.add(finalProjection);

		ArrayNode publisherNodes = createPublishers(publishers, lastProcessorId);
		result.addAll(publisherNodes);
		return result;
	}
	
	private ObjectNode createInputProcessors(List<String> dataSources, String groupId, String tenantId) throws CPSException{
		// TODO: support multiple datasource, that should be a join
		if(dataSources.size() > 1){
			throw new CPSException(ErrorCode.INPUT_ERROR, "Selecting from multiple datasources is not supported by now");
		}
		ObjectNode inputProcessor = metaCreator.createInputProcessor(dataSources.get(0));
		return inputProcessor;
	}
	
	private ObjectNode createCalculationNode(List<String> datasources, SelectItemDescriptor selectItems, AggregationDescriptor aggregation, BooleanExpression filterCondition, String parentProcessorId) throws CPSException{
		ProjectionParser projectionParser = new ProjectionParser(tenantId, datasourceFactory, schemaCache);
		Set<DatasourceField> fields = projectionParser.parseProjection(datasources, selectItems, aggregation, filterCondition);
		Map<String, ExpressionNode> fieldMapping = new HashMap<>();
		for (DatasourceField field : fields) {
			fieldMapping.put(field.fullQualifiedName(), new IdentifierNode(field.fullQualifiedName()));
		}
		Map<String, ValueExpression> items = selectItems.getColumnExpression();
		//Alias will override datasource fields if some select items are without alias
		for (String alias : items.keySet()) {
			ValueExpression expression = items.get(alias);
			fieldMapping.put(alias, expression);
		}
		ObjectNode calculationNode = metaCreator.createScalarCalculationNode(fieldMapping, parentProcessorId);
		return calculationNode;
	}

	private ObjectNode createFinalProjection(List<String> datasources, SelectItemDescriptor projection, String parentProcessorId) throws CPSException{
		Map<String, String> fieldMapping = new HashMap<>();
		if(projection.isGlobalSelectAll()){
			for (String datasource : datasources) {
				BasicDataSourceMetadata metadata = datasourceFactory.getDataSourceMetadata(datasource);
				ITimeSeriesSchema schema = schemaCache.getSchema(tenantId, metadata.getSchemaid(), new ArrayList<String>());
				String[] fieldNames = schema.getTagNames();
				for (String field : fieldNames) {
					String fullName = SqlUtil.fullName(datasource, field);
					fieldMapping.put(fullName, fullName);
				}
			}
		}else{
			List<String> allSelectedDatasources = projection.getSelectAllTables();
			for (String datasource : allSelectedDatasources) {
				BasicDataSourceMetadata metadata = datasourceFactory.getDataSourceMetadata(datasource);
				ITimeSeriesSchema schema = schemaCache.getSchema(tenantId, metadata.getSchemaid(), new ArrayList<String>());
				String[] fieldNames = schema.getTagNames();
				for (String field : fieldNames) {
					String fullName = SqlUtil.fullName(datasource, field);
					fieldMapping.put(fullName, fullName);
				}
			}
			Map<String, ValueExpression> selectItems = projection.getColumnExpression();
			for (String alias : selectItems.keySet()) {
				fieldMapping.put(alias, alias);
			}
		}
		ObjectNode projectionNode = metaCreator.createProjectionWithoutCalculation(fieldMapping, parentProcessorId);
		return projectionNode;
	}
	
	private ArrayNode createPublishers(List<String> publisherNames, String parentProcessorId){
		ArrayNode nodes = JSONUtility.newArrayNode();
		for (String p : publisherNames) {
			ObjectNode publisherNode = metaCreator.createPublishNode(p,
					parentProcessorId);
			nodes.add(publisherNode);
		}
		return nodes;
	}

}
